系列說明 >> 本系列大部分 PR 來自私人或公司專案,部分程式碼、環境設定與實作細節不便公開。
我只能保證文中提到的 PR 與問題皆為真實案例,但會經過必要的匿名化與內容調整。
本系列主要分享問題如何被發現、背後的思考方式,以及解法如何形成,不會深入討論完整實作與部署細節,敬請見諒。
Change Ref: PR #168-feat(audit): Phase 2a Gate C 唯讀 sub-agent adversarial refuter
Issue: Agent 可以根據 Workbook、Source、Live Evidence、Negative Control 與 Dual Anchor 建立 Pass Criteria,但最後仍可能由同一個 Agent 判斷自己的 Criteria 是否真的被 Source 支持。
Root Cause: 「有 Citation」與「真的讀過 Citation 並試圖推翻 Criteria」是兩件事;如果 Refuter 的閱讀行為也只靠 Agent 自述,Independent Review 只是多一份比較正式的作文。
Solution: 加入 Gate C,以獨立唯讀 Sub-agent 做 Adversarial Refutation;有效 Vote 必須有 Machine-observed Tool Call、確實讀到指定 Citation。Quorum 足夠且沒有有效 Refutation 才能 PASS,Spawner 不可用則 Fail Closed。
Evidence: Audit Tests 366 passed, 1 skipped、Policy Check fail 0;PR 開發紀錄記載 Claude Headless Live 驗證取得 tool_calls=1、inspected=[README.md] 與 Structured Verdict。PR Review 另外抓出 Route C 已上線,但 Prompt、README 與 Plan 還殘留 Broker/bwrap 舊路徑的 Drift。
上一篇之後,一條 Case 想進 applied 已經不太容易。
好的值要 PASS,壞的值要 FAIL;Criteria 要有 Anchor;Evidence 還得跟目前這份 Proposed YAML、Verify-edit 與 RID 對得起來。
而 needs_pass3,就是前兩輪機械比對還無法定案,必須交給 Agent 結合 Source、實機 Evidence 與語意判斷繼續校正的 Case。
問題就出在最後四個字。
語意判斷。
Hash 可以機械比,壞值有沒有 Fail 可以機械測。
但當 Criteria 寫著:
這個欄位應該等於某個 Driver 狀態。
最後還是得有人讀 Source,確認 Driver 狀態是不是真正的 Authority。
通常那個「有人」,就是 Agent。
Agent 看 Source
→ Agent 提出 Criteria
→ Agent 整理 Citation
→ Agent 跑完 A/B/D
→ Agent:我確認這個 Criteria 合理
我盯著最後一行。
「誰確認的?」
Agent:「我。」
……
我們花了十四天把 trust me 一層一層拆掉。
最後在最貴的那一層,又把它裝回去了。
老Go很準時:
「所以現在是出題的人自己找參考書、自己寫答案、自己改考卷,最後再蓋一個『經本人審核無誤』?」
我正準備反駁。
他又補了一句:
「球證、旁證加上主辦、協辦所有單位都是我的人,怎麼和我鬥?」
Agent 也不是亂搞:
「B、A、D 都是 deterministic gate,Citation 也綁到 Source Root。我的判斷不是憑空生成。」
對。
麻煩就在這裡。
Source 真的、Live Evidence 真的、Hash 真的、Citation 也真的,最後那一步 Interpretation 歪掉一點,你還不一定第一眼看得出來。
所以真正要查的是:
它對 Source 的解讀,有沒有漏掉反證?
最直覺的答案:
再找一個 Agent Review。
第一個建 Criteria,第二個看 Source,第二個說 OK。
Independent Review,收工。
老Go只問:
「第二個 Agent 說它看過 Source,你信?」
幹。
又來。
如果第二個 Agent 最後只回:
I reviewed the cited source.
The criterion looks correct.
那只是把:
Agent A:相信我。
Agent B:我也相信他。
Agent C:俺也一樣。
三票通過。
程序非常完整。
Correctness 不知去向。
Agent 提出比較工程的版本:
「那要求 Refuter 回報它看過哪些檔案、哪些行號。」
以前我其實很愛這套。
不要只跟我說「看過」,把 FILE:FUNCTION:LINE 列出來,我至少還能順著位置回去抽查。
但如果 inspected=["foo.c:123"] 也是 Agent 自己寫進 JSON,Authority 根本沒有換人。
我要的不是更詳細的讀書心得。
我要外面有人能回答:
你剛才到底有沒有真的去讀?
所以 Gate C 第一個要解的,不是「Refuter 夠不夠聰明」。
而是 Liveness。
PR #168 最後把有效 Vote 的基本條件卡成:
tool_calls > 0
inspected ∩ citations != ∅
不只要有 Tool Call,還得真的碰到這個 Case 引用的 Citation。
全程只看 README,Case Citation 卻在 Driver Source,那張票不算。
如果它說「我找到反證」,也得指回實際看過的 Source。
老Go:
「所以刷卡進公司,不代表有上班;但連卡都沒刷,就先不要報加班?」
差不多。
Machine-observed Tool Call 只證明:
它真的去過。
不是:
它一定看懂。
Boundary 先卡在這裡,不多送。
一開始 Agent 提的做法更硬。
既然不相信 Refuter 自己報 Tool Call,那乾脆關進 Sandbox:Source Read-only,所有讀取經過 Broker,Host 端留下 Log。
我看著架構圖,覺得很漂亮。
老Go看著架構圖,覺得我們有病。
「你現在要解的問題是?」
「確認 Refuter 真的有讀 Source。」
「然後你做了 Broker、Bridge、Launcher、Sandbox?」
「Boundary 要乾淨。」
「喔,所以為了查門禁紀錄,你決定先蓋一棟辦公大樓。」
真正替他講話的是 Live Test。
bwrap 路線碰到 Claude CLI 後開始撞牆:claude.exe、Node、Config、Runtime Dependency 要塞進最小 Sandbox 沒那麼單純;--unshare-net 更直接。
網路切掉,Refuter 確實非常安全。
安全到連模型 Backend 都摸不到。
Agent:
「可以繼續補 Mount 跟 Network。」
老Go:
「你只是要它讀檔。」
幹。
不是 Sandbox 不好,是 Scope 已經從門禁卡長成都市計畫。
做到這裡我才反應過來:我幹嘛再打造一套 CLI 本來就有的東西,只為了證明「有沒有讀 Code」?
所以最後改用各家 CLI 自己的 Headless/Sub-agent 機制,第一個 Adapter 是 Claude Headless。
Refuter 只給唯讀能力:
Read
Grep
Glob
Claude CLI 本來就會在 stream-json 留下 Tool Use Event,那就直接解析:
tool_calls
inspected
structured verdict
Authority 終於拆開:
Refuter → 判斷 Source 有沒有反證
Vendor CLI → 證明它實際用了哪些 Tool
Gate C → 判斷這張 Vote 有沒有資格被算
老Go問:
「所以前面那棟監獄?」
「拆掉。」
「Broker?」
「拆。」
「Bridge?」
「拆。」
「Launcher?」
「也拆。」
他點點頭。
「很好,今天終於不是新增檔案數量決定 Architecture Quality。」
Liveness 解掉以後,下一題是怎麼計票。
假設有三個 Refuter:
A:沒看到問題
B:沒看到問題
C:我在 Citation 裡找到一段,跟這條 Criteria 直接衝突
老Go:
「二比一,所以 PASS?」
Agent:
「不能這樣算。」
「為什麼?」
「A 跟 B 只是『我沒找到』。」
「C 如果真的拿得出 Source 證明 Criteria 有問題,那就是反例。」
「兩張『我沒看到』,不能把一張『證據在這裡』投票投掉。」
這樣我就懂了。
三個人一起找 Bug:
A:我沒找到
B:我也沒找到
C:Bug 在 foo.c:123,條件跟 Criteria 相反
正常人不會回:
「不好意思,二比一,你的 Bug 不成立。」
Source 又沒有投票權。
所以 Gate C 不是 Majority Vote,而是 Difficult-pass:
valid_votes >= quorum
AND
refuted == 0
第一個條件,是確定真的有足夠數量的 Refuter 完成有效 Review。
不能叫三個 Agent,兩個根本沒讀 Citation,只剩一個回「Looks good」,然後宣布 Independent Review 完成。
第二個更重要:只要有一張成立的反證,Gate C 就不能 PASS。
老Go馬上抓另一邊:
「那我每一案都喊『我反對』,不就全部卡死?」
Agent:
「所以不是喊 Refuted 就算 Refuted。」
「要怎樣才算?」
「真的讀到 Citation,再指出哪一段 Source 跟 Criteria 衝突,而且位置要出現在 CLI 實際觀測到的
inspected裡。」
也就是:
想投「沒問題」
→ 真的讀過 Citation,Vote 才有效
想投「有問題」
→ 除了讀過,還得把反證位置拿出來
不是少數服從多數。
而是:
先確定有足夠的人真的做過 Review;只要其中有人拿出成立的反證,就先停。
老Go:
「很好,至少三個 Agent 一起講幹話,不會因為人多就自動變 RFC。」
這個我同意。
還有一個洞。
如果所有 Refuter 都真的讀了 Citation,最後卻永遠回答:
No issue found.
形式全對,內容全睡著。
所以 Gate C 還放了一個 Planted Negative Control:故意塞一個已知應該被 Refute 的問題。
注意,這不是上一篇 Gate B 那個「好值要 PASS、壞值要 FAIL」。
這次是在測 Refuter。
如果連故意放進去的問題都抓不到,整輪作廢。
連我丟在桌上的香蕉皮都沒看到,你們剛才到底在 Review 三小?
Agent:
「等於先驗 Refuter 有沒有 Refute 能力。」
老Go:
「Test 測產品,Audit 測 Test,Gate C 測 Agent,現在再出題測 Gate C。」
「對。」
「那誰測你?」
「下一題。」
Route C 定案後,我原本以為剩下就是收尾。
結果 PR Review 第一刀就往自己身上捅。
實作已經改成 Read/Grep/Glob,Prompt 卻還在叫 Refuter 用舊的 mcp__broker__*。
Architecture:Route C
Prompt:Route B 時空
照那份 Prompt 真跑下去:
tool_calls = 0
→ Vote invalid
→ Gate C inconclusive
我們剛做完一個 Gate,規定:
「不要跟我說你有看,Tool Call 拿出來。」
然後它自己的 Prompt 指向已經不存在的 Tool。
Repo 很配合,當場示範為什麼不能只相信設計文件。
而且不只 Prompt。README、Plan、Integration Test 的描述,都還有 Broker/bwrap 的殘影。
Code 已經搬家,文件還在舊地址收信。
Copilot Review 20 個 Changed Files,最後丟了 7 個 Comment,主要就在抓這批 Route Drift。
Independent Review 不是再找一個比較聰明的 Agent 幫你說 OK。
是找一個沒有參與你那套自我解釋的人,專門問:
你是不是漏了什麼?
Gate C 本身,就是先被這個方法打了一巴掌才合併。
PR #168 最後的 Audit Tests:
366 passed, 1 skipped
Policy Check:
fail 0
PR 開發紀錄也記載 Claude Headless Live 驗證取得:
tool_calls = 1
inspected = [README.md]
Structured Verdict 能從 stream-json 正常解析。
這些 Evidence 支持 Route C 的 Spawner、Tool-call Observation 與 Gate Logic 有跑起來。
但還是那句:
有 Tool Call,不等於看懂 Source。
Gate C 只是把:
「我有看,而且我覺得沒問題。」
拆成:
CLI:我看到它真的讀了這些東西。
Refuter:我沒有找到有效反證。
Gate:這張票符合規則,可以算。
沒有任何一個 Agent 可以自己包辦三句。
這才是今天真正換掉的 Authority。
做到這裡,我們至少把一件事拆乾淨了。
以前是一個 Agent 從頭包到尾:
看 Source
→ 寫 Criteria
→ 跑 Test
→ 看 Evidence
→ 說自己沒看出問題
→ PASS
現在不是。
Proposer
→ 提出 Criteria
Executor
→ 跑實際 Test
Refuter
→ 專門找 Criteria 的反證
Vendor CLI
→ 留下 Refuter 到底看過什麼的紀錄
Gate
→ 決定這張 Vote 能不能算
每個角色能證明的事情都變少了。
反而比較可信。
老Go看完流程,沉默了一下。
「恭喜。」
「難得,你居然會稱讚人。」
「你成功把一個什麼都自己說了算的 Agent,拆成四、五個彼此不能互相相信的 Agent。」
「……這叫 Separation of Responsibility。」
「好。」
「那誰管他們?」
我停了一下。
「什麼意思?」
「誰知道哪個 Agent 現在在跑哪件事?」
「誰知道它跑完了還是死了?」
「Executor 做完之後,誰叫 Refuter 接手?」
「Refuter 卡住,誰 Retry?」
「Gate 過了,誰把下一步放行?」
「還是你準備開五個 Terminal,然後自己盯?」
……
幹。
這題好像比剛才那題還大。
回頭看 Day 13 到 Day 15,我們其實一直在做同一件事:
Workbook 有沒有選對
↓
Criteria 會不會只會 PASS
↓
Citation 到底支不支持判斷
↓
Auditor 到底有沒有真的看 Source
每往下一層,都是在拔掉一個:
「相信我,我有做。」
到 Gate C,連 Auditor 自己都不能靠這句話過關。
這也差不多是 Audit 這一幕該停的地方了。
因為再往下,問題已經不是某一條 Test Case,也不是再補一個 Gate。
而是整個工作現場開始同時存在:
提出方案的人
執行的人
找碴的人
判斷 Gate 的人
最後要接手的人
角色拆開是好事。
但拆開之後,工作不會自己流動。
老Go:
「所以多 Agent 的問題解決了?」
「沒有。」
「只是 Agent 變多了?」
「……對。」
「那不就多開幾個 pane?」
這次換我看著他。
對。
多開幾個 Agent,不叫 Orchestration。
如果一份工作沒有自己的 Identity,不知道現在在哪個 State、誰正在做、失敗後由誰接、完成後又該交給誰,那只是同時開了更多事故現場。
Audit 解決的是:
這個結果憑什麼相信?
下一個問題則是:
當這些彼此獨立的角色開始同時工作,誰負責讓整個 Workflow 活著跑完?
從下一篇開始,我們先離開 Test Case。
來管 Agent。
Have a nice day.